#include <stdio.h>
#include <stdlib.h>
#include <memory>

#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <stb_image.h>

#include "objectfactory.h"
#include "model.h"
#include "node.h"

static GLfloat groundVertices[] = {
    -0.5f, -0.5f, -0.5f, 0.0f, 1.0f,
    -0.5f, -0.5f,  0.5f, 0.0f, 0.0f,
     0.5f, -0.5f, -0.5f, 1.0f, 1.0f,
    -0.5f, -0.5f,  0.5f, 0.0f, 0.0f,
     0.5f, -0.5f, -0.5f, 1.0f, 1.0f,
     0.5f, -0.5f,  0.5f, 1.0f, 0.0f,
};

static GLchar *varis[] = {"outPos"};

class Feedback : public Node
{
public:
    Feedback();
    virtual ~Feedback();

public:
    virtual bool startup();
    virtual void render(double currentTime, double difTime);
    virtual void shutdown();

private:
    void initData();
    void makeGroundVao();
    void makeGroundTex();
    bool makeGroundProgram();
    void loadFootballModel();
    bool makeFootballProgram();
    void makeTfbo();
    bool checkFootballTouched(const std::vector<vmath::vec4> &groundVertices, \
                              const std::vector<vmath::vec4> &footballVertices);
    void drawGround();
    void drawFootball();
    void moveUp(double difTime);
    void moveDown();
    void changeFootballSpeed(double difTime);
    void changeFootballPos();

private:
    const GLuint GROUND_VERTEX_SIZE = 6;
    glm::vec3 curGroundMovePos = glm::vec3(0.0f, 0.0f, 0.0f);
    glm::mat4 groundTrans = glm::mat4(1.0f);

    glm::vec3 viewPos = glm::vec3(0.0f, -0.3f, 0.9f);
    glm::vec3 cameraZAxis = glm::vec3(0.0f, 0.0f, -1.0f);
    glm::vec3 cameraXAxis = glm::vec3(1.0f, 0.0f, 0.0f);
    glm::vec3 cameraYAxis = glm::vec3(0.0f, 1.0f, 0.0f);

    glm::vec3 footballMoveDir = glm::vec3(0.0f, -1.0f, 0.0f);
    glm::vec3 curFootballMovePos = glm::vec3(0.0f, 0.3f, 0.0f);
    glm::mat4 footballTrans = glm::mat4(1.0f);
    GLdouble fMoveSpeed = 0;
    GLint speedDir = 1;
    GLint aSpeed = 1;
    bool footballTouched = false;

    bool isChangeDir = false;
    bool downBool = true;
    bool isMove = true;

    std::vector<vmath::vec4> groundFeedbackOut;
    std::vector<vmath::vec4> footballFeedbackOut;

private:
    GLuint groundVao = 0;
    GLuint groundVbo = 0;
    GLuint groundTexId = 0;

    std::unique_ptr<Model> football;

    GLSLProgram groundProgram;
    GLSLProgram footballProgram;

    GLuint groundTfbo = 0;
};

Feedback::Feedback()
{
    nodeName = "Feedback";
}

Feedback::~Feedback()
{
    shutdown();
}

void Feedback::initData()
{
    curGroundMovePos = glm::vec3(0.0f, 0.0f, 0.0f);
    groundTrans = glm::mat4(1.0f);

    viewPos = glm::vec3(0.0f, -0.3f, 0.9f);
    cameraZAxis = glm::vec3(0.0f, 0.0f, -1.0f);
    cameraXAxis = glm::vec3(1.0f, 0.0f, 0.0f);
    cameraYAxis = glm::vec3(0.0f, 1.0f, 0.0f);

    footballMoveDir = glm::vec3(0.0f, -1.0f, 0.0f);
    curFootballMovePos = glm::vec3(0.0f, 0.3f, 0.0f);
    footballTrans = glm::mat4(1.0f);
    fMoveSpeed = 0;
    speedDir = 1;
    aSpeed = 1;
    footballTouched = false;

    isChangeDir = false;
    downBool = true;
    isMove = true;
}

bool Feedback::startup()
{
    /*
     * glBeginTransformFeedback in 3.0
     * glEndTransformFeedback in 3.0
     * glTransformFeedbackVaryings in 3.0
    */
    if (PerfWindow::glVersion < 330) {
        printf("Failed! The GL version is less than 3.3 !\n");
        return false;
    }
    if (!makeGroundProgram()) {
        return false;
    }
    if (!makeFootballProgram()) {
        return false;
    }
    initData();
    makeGroundVao();
    makeGroundTex();
    loadFootballModel();
    football->enableFeedback();
    makeTfbo();
    return true;
}

void Feedback::render(double currentTime, double difTime)
{
    (void)currentTime;
    glEnable(GL_DEPTH_TEST);
    glClearColor(0.6f, 0.7f, 0.8f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    glDisable(GL_CULL_FACE);
    drawGround();
    drawFootball();

    if (isMove) {
        if (checkFootballTouched(groundFeedbackOut, footballFeedbackOut)) {
            if (!footballTouched) {
                moveUp(difTime);
            }
        } else {
            if (footballTouched) {
                footballTouched = false;
            }
        }

        if (fMoveSpeed <= 0 && !downBool) {
            moveDown();
        }
        changeFootballSpeed(difTime);
        changeFootballPos();

        if (footballTouched && speedDir > 0) {
            isMove = false;
        }
    }
}

void Feedback::shutdown()
{
    glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    glDisable(GL_DEPTH_TEST);
    if (groundVao != 0) {
        glDeleteVertexArrays(1, &groundVao);
        groundVao = 0;
    }
    if (groundVbo != 0) {
        glDeleteBuffers(1, &groundVbo);
        groundVbo = 0;
    }
    if (groundTexId != 0) {
        glDeleteTextures(1, &groundTexId);
        groundTexId = 0;
    }
    if (groundTfbo != 0) {
        glDeleteBuffers(1, &groundTfbo);
        groundTfbo = 0;
    }
    if (football) {
        football->clearModel();
    }
    groundProgram.clear();
    footballProgram.clear();
}

void Feedback::makeGroundVao()
{
    if (groundVao <= 0) {
        glGenVertexArrays(1, &groundVao);
    }
    glBindVertexArray(groundVao);

    if (groundVbo <= 0) {
        glGenBuffers(1, &groundVbo);
    }
    glBindBuffer(GL_ARRAY_BUFFER, groundVbo);

    glBufferData(GL_ARRAY_BUFFER, sizeof(groundVertices), groundVertices, GL_STATIC_DRAW);
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(GLfloat), nullptr);
    glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(GLfloat), (void *)(3 * sizeof(GLfloat)));
    glEnableVertexAttribArray(0);
    glEnableVertexAttribArray(1);
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glBindVertexArray(0);
}

void Feedback::makeGroundTex()
{

    int width = 0;
    int height = 0;
    int comp = 0;
    unsigned char *data = stbi_load("../media/textures/floor01.png", &width, &height, &comp, 0);
    if (data != nullptr) {
        GLenum format = GL_RED;
        if (comp == 1) {
            format = GL_RED;
        } else if (comp == 3) {
            format = GL_RGB;
        } else if (comp == 4) {
            format = GL_RGBA;
        }
        if (groundTexId == 0) {
            glGenTextures(1, &groundTexId);
        }
        glBindTexture(GL_TEXTURE_2D, groundTexId);

        glTexImage2D(GL_TEXTURE_2D, 0, static_cast<int>(format), width, height, \
                     0, format, GL_UNSIGNED_BYTE, data);
        glGenerateMipmap(GL_TEXTURE_2D);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_MIRRORED_REPEAT);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
        stbi_image_free(data);
        data = nullptr;
    } else {
        std::cout << "feedback load texture failed " << std::endl;
    }
    glBindTexture(GL_TEXTURE_2D, 0);
}

bool Feedback::makeGroundProgram()
{
    glTransformFeedbackVaryings(groundProgram.getProgram(), 1, varis, GL_INTERLEAVED_ATTRIBS);

    if (!groundProgram.build("../media/shaders/feedback/ground_vs.glsl.txt", \
                             "../media/shaders/feedback/ground_fs.glsl.txt")) {
        return false;
    }
    glm::mat4 groundTrans = glm::mat4(1.0);
    groundProgram.use();
    groundProgram.setUniformMatrix4fv("trans", glm::value_ptr(groundTrans));

    glm::mat4 view = glm::lookAt(viewPos, viewPos + glm::normalize(cameraZAxis), cameraYAxis);
    groundProgram.setUniformMatrix4fv("view", glm::value_ptr(view));

    glm::mat4 projection = glm::mat4(1.0f);
    projection = glm::perspective(glm::radians(45.0f), \
                                  static_cast<float>(getWidth()) / static_cast<float>(getHeight()), \
                                  0.1f, 10000.0f);
    groundProgram.setUniformMatrix4fv("projection", glm::value_ptr(projection));
    return true;
}

void Feedback::loadFootballModel()
{
    football.reset(new Model);
    football->loadModel("../media/objects/football.obj");
    football->setUseGLPath(GL_FALSE);
}

bool Feedback::makeFootballProgram()
{
    glTransformFeedbackVaryings(footballProgram.getProgram(), 1, varis, GL_INTERLEAVED_ATTRIBS);
    if (!footballProgram.build("../media/shaders/feedback/football_vs.glsl.txt", \
                               "../media/shaders/feedback/football_fs.glsl.txt")) {
        return false;
    }
    footballTrans = glm::mat4(1.0f);
    footballTrans = glm::translate(footballTrans, curFootballMovePos);
    footballTrans = glm::scale(footballTrans, glm::vec3(0.05f, 0.05f, 0.05f));

    glm::mat4 view = glm::lookAt(viewPos, viewPos + glm::normalize(cameraZAxis), cameraYAxis);

    glm::mat4 projection = glm::mat4(1.0f);
    projection = glm::perspective(glm::radians(45.0f), \
                                  static_cast<float>(getWidth()) / static_cast<float>(getHeight()), \
                                  0.1f, 10000.0f);
    footballProgram.use();
    footballProgram.setUniformMatrix4fv("trans", glm::value_ptr(footballTrans));
    footballProgram.setUniformMatrix4fv("view", glm::value_ptr(view));
    footballProgram.setUniformMatrix4fv("projection", glm::value_ptr(projection));
    return true;
}

void Feedback::makeTfbo()
{
    if (groundTfbo == 0) {
        glGenBuffers(1, &groundTfbo);
    }
    glBindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, groundTfbo);
    glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER,
                 static_cast<GLuint>(GROUND_VERTEX_SIZE) * sizeof(glm::vec4), \
                 nullptr, GL_DYNAMIC_COPY);
    glBindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, 0);
}

bool Feedback::checkFootballTouched(const std::vector<vmath::vec4> &groundVertices, \
                                    const std::vector<vmath::vec4> &footballVertices)
{
    GLfloat groundY = groundVertices[0][1];
    for (uint64_t i = 0; i < footballVertices.size(); i++) {

        if (fabs(footballVertices[i][1] - groundY) <= 0.00001f) {
            return true;
        }
    }
    return false;
}

void Feedback::drawGround()
{
    groundProgram.use();
    glBindVertexArray(groundVao);
    glBindTexture(GL_TEXTURE_2D, groundTexId);
    glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, groundTfbo);
    glBeginTransformFeedback(GL_TRIANGLES);
    glDrawArrays(GL_TRIANGLES, 0, static_cast<GLint>(GROUND_VERTEX_SIZE));
    glEndTransformFeedback();
    glBindVertexArray(0);
    glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, 0);

    glBindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, groundTfbo);
    vmath::vec4 *pos = static_cast<vmath::vec4 *>(glMapBufferRange(GL_TRANSFORM_FEEDBACK_BUFFER, 0,
                                                                   GROUND_VERTEX_SIZE * sizeof(GLfloat) * 4, GL_MAP_READ_BIT));
    groundFeedbackOut.clear();
    groundFeedbackOut.reserve(GROUND_VERTEX_SIZE);
    for (GLuint i = 0; i < GROUND_VERTEX_SIZE; i++) {
        groundFeedbackOut.push_back(pos[i]);
    }
    glUnmapBuffer(GL_TRANSFORM_FEEDBACK_BUFFER);
    glBindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, 0);
}

void Feedback::drawFootball()
{
    footballTrans = glm::mat4(1.0f);
    footballTrans = glm::translate(footballTrans, curFootballMovePos);
    footballTrans = glm::scale(footballTrans, glm::vec3(0.05f, 0.05f, 0.05f));
    footballProgram.use();
    footballProgram.setUniformMatrix4fv("trans", glm::value_ptr(footballTrans));
    football->renderModel();
    footballFeedbackOut.clear();
    footballFeedbackOut = football->getFeedbackOut();
}

void Feedback::moveUp(double difTime)
{
    footballTouched = true;
    isChangeDir = true;
    downBool = false;
    speedDir = -1.0f;
    fMoveSpeed -= (static_cast<GLdouble>(aSpeed) * difTime * 0.1);
}

void Feedback::moveDown()
{
    isChangeDir = true;
    speedDir = 1.0f;
    downBool = true;
    fMoveSpeed = 0.0;
}

void Feedback::changeFootballSpeed(double difTime)
{
    if (isChangeDir) {
        footballMoveDir.y = -(footballMoveDir.y);
        isChangeDir = false;
    } else {
        fMoveSpeed += static_cast<GLdouble>(aSpeed * speedDir) * difTime * 0.1;
    }
}

void Feedback::changeFootballPos()
{
    GLfloat tempSpeed = static_cast<GLfloat>(fMoveSpeed);
    curFootballMovePos = (footballMoveDir * tempSpeed) + curFootballMovePos;
    if (curFootballMovePos.y > 0.3f) {
        curFootballMovePos.y = 0.3f;
    }
    if (curFootballMovePos.y < (-0.45f)) {
        curFootballMovePos.y = (-0.45f);
    }
}

REGISTER_OBJECT(Node, Feedback)
